
/**
 * Copyright 2009 cestum.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
package org.waveprotocol.wave.model.document.operation.impl {
import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.AttributesUpdate;
import org.waveprotocol.wave.model.document.operation.BufferedDocInitialization;
import org.waveprotocol.wave.model.document.operation.BufferedDocOp;
import org.waveprotocol.wave.model.document.operation.DocInitialization;
import org.waveprotocol.wave.model.document.operation.DocumentOperation;
import org.waveprotocol.wave.model.document.operation.DocumentOperationCursor;
import org.waveprotocol.wave.model.document.operation.EvaluatingDocOpCursor;
import org.waveprotocol.wave.model.document.operation.algorithm.AnnotationsNormalizer;
import org.waveprotocol.wave.model.document.operation.algorithm.RangeNormalizer;
import org.waveprotocol.wave.model.document.operation.impl.impl.ConciseDocOpCursor;
import org.waveprotocol.wave.model.document.operation.impl.impl.CursorAdapterBufferedDocInitialization;
import org.waveprotocol.wave.model.document.operation.impl.impl.CursorAdapterDocInitializtion;
import org.waveprotocol.wave.model.document.operation.impl.impl.InitialDocOpCursor;
import org.waveprotocol.wave.model.document.operation.impl.impl.ResultingDocOpCursor;
import org.waveprotocol.wave.model.document.operation.impl.impl.XMLDocInitializationCursor;
import org.waveprotocol.wave.model.document.operation.impl.impl.XMLDocInitializtion;
import org.waveprotocol.wave.model.document.operation.util.ExplodedDocOp;
import org.waveprotocol.wave.model.operation.OpCursorException;
import org.waveprotocol.wave.model.util.RuntimeError;
import org.waveprotocol.wave.model.util.StringBuilder;

public class DocumentOperationUtil {
	
	public static var PI_TARGET:String= "a";
	
	public function buffer(o:Object):Object {  
		// figure out what the type of o actually is and call our  
		// private helper function based on that  
		if (o is DocumentOperation) {  
			return buffer_docOp(DocumentOperation(o));  
		} else if ( o is DocInitialization) {  
			return buffer_DocInit(DocInitialization(o));  
		} else {  
			throw new Error("DocOpUtil.buffer  invalid type " + typeof(o));  
		}  
	}  
	
	
	public static function buffer_docOp(op:DocumentOperation):BufferedDocOp {
		if (op instanceof BufferedDocOp) {
			return BufferedDocOp(op);
		}
		var b:DocOpBuffer= new DocOpBuffer();
		op.apply(b);
		return b.finish();
	}
	
	public static function buffer_DocInit(m:DocInitialization):BufferedDocInitialization {
		if (m instanceof BufferedDocInitialization) {
			return BufferedDocInitialization(m);
		}
		var b:DocInitializationBuffer= new DocInitializationBuffer();
		m.applyDocInit(b);
		return b.finish();
	}
	
	
	public static function asInitialization(o:Object):Object {  
		// figure out what the type of o actually is and call our  
		// private helper function based on that  
		if (o instanceof DocumentOperation) {  
			return asInitialization_docOp(DocumentOperation(o));  
		} else if (o instanceof BufferedDocOp ) {  
			return asInitialization_BufferedDocOP(BufferedDocOp(o));  
		} else {  
			throw new Error("DocOpUtil.buffer  invalid type " + typeof(o));  
		}  
	}  
	
	
	public static function asInitialization_docOp(op:DocumentOperation):DocInitialization {
		if (op instanceof DocInitialization) {
			return DocInitialization(op);
		} else {
			return new CursorAdapterDocInitializtion(op);
		}
	}
	
	public static function asInitialization_BufferedDocOP(op:BufferedDocOp):BufferedDocInitialization {
		if (op instanceof BufferedDocInitialization) {
			return BufferedDocInitialization(op);
		} else {
			return new CursorAdapterBufferedDocInitialization(op);
		}
	}
	
	public static function createConciseStringBuilder(op:DocumentOperation, b:StringBuilder):DocumentOperationCursor {
		return new ConciseDocOpCursor(b);
	}
	
	public static function toConciseString(o:Object):String {  
		// figure out what the type of o actually is and call our  
		// private helper function based on that  
		if (o is DocumentOperation) {  
			return toConciseString_docop(DocumentOperation(o));  
		} else if (o is Attributes) {  
			return toConciseString_attrib(Attributes(o));  
		} else if ( o is AttributesUpdate) {  
			return toConciseString_attribUpdate(AttributesUpdate(o));  
		} else if ( o is AnnotationBoundaryMap) {  
			return toConciseString_annotation(AnnotationBoundaryMap(o));  
		} else {  
			throw new Error("DocOpUtil.buffer  invalid type " + typeof(o));  
		}  
	}  
	
	public static function toConciseString_docop(op:DocumentOperation):String {
		var b:StringBuilder= new StringBuilder();
		op.apply(createConciseStringBuilder(op, b));
		return b.toString();
	}
	
	public static function toConciseString_attrib(attributes:Attributes):String {
		if (attributes.isEmpty()) {
			return "{}";
		}
		var b:StringBuilder= new StringBuilder();
		b.append("{ ");
		var first:Boolean= true;
		for (var key:String in attributes.keySet()) {
			if (first) {
				first = false;
			} else {
				b.append(", ");
			}
			b.append(key);
			b.append("=");
			b.append(literalString(String(attributes.getValue(key))));
		}
		b.append(" }");
		return b.toString();
	}
	
	public static function toConciseString_attribUpdate(update:AttributesUpdate):String {
		if (update.changeSize() == 0) {
			return "{}";
		}
		var b:StringBuilder= new StringBuilder();
		b.append("{ ");
		for (var i:int= 0; i < update.changeSize(); ++i) {
			if (i > 0) {
				b.append(", ");
			}
			b.append(update.getChangeKey(i));
			b.append(": ");
			b.append(literalString(update.getOldValue(i)));
			b.append(" -> ");
			b.append(literalString(update.getNewValue(i)));
		}
		b.append(" }");
		return b.toString();
	}
	
	public static function toConciseString_annotation(map:AnnotationBoundaryMap):String {
		var b:StringBuilder= new StringBuilder();
		b.append("{ ");
		var notEmpty:Boolean= false;
		for (var i:int= 0; i < map.endSize(); ++i) {
			if (notEmpty) {
				b.append(", ");
			} else {
				notEmpty = true;
			}
			b.append(literalString(map.getEndKey(i)));
		}
		for (var i:int= 0; i < map.changeSize(); ++i) {
			if (notEmpty) {
				b.append(", ");
			} else {
				notEmpty = true;
			}
			b.append(literalString(map.getChangeKey(i)));
			b.append(": ");
			b.append(literalString(map.getOldValue(i)));
			b.append(" -> ");
			b.append(literalString(map.getNewValue(i)));
		}
		b.append(" }");
		return notEmpty ? b.toString() : "{}";
	}
	
	private static function escapeLiteral(string:String):String {
		return string.replace("\\", "\\\\").replace("\"", "\\\"");
	}
	
	public static function literalString(string:String):String {
		return (string == null) ? "null" : "\"" + escapeLiteral(string) + "\"";
	}
	
	public static function toXmlString(op:DocInitialization):String {
		return toPrettyXmlString(op, -1);
	}
	
	public static function toPrettyXmlString(op:DocInitialization, indent:int):String {
		var b:StringBuilder= new StringBuilder();
		buildXmlString(op, indent, b);
		return b.toString();
	}
	
	public static function buildXmlString(op:DocInitialization, indent:int,
										  b:StringBuilder):void {
		
		try {
			op.applyDocInit(new XMLDocInitializationCursor(b));
		} catch (e:RuntimeError) {
			throw new RuntimeError("toXmlString: DocInitialization was probably ill-formed");
		}
	}
	
	public static function debugToXmlString(op:DocInitialization):String {
		try {
			return toXmlString(op);
		} catch (e:OpCursorException) {
			return "toXmlString: DocInitialization was internally broken. ";
		}
		return null;
	}
	
	public static function attributeString(attributes:Attributes):String {
		var b:StringBuilder= new StringBuilder();
		var first:Boolean= true;
		for (var key:String in  attributes.keySet()) {
			if (first) {
				first = false;
			} else {
				b.append(" ");
			}
			b.append(key + "=" + (key == null ? "null"
				: "\"" + xmlAttrEscape(String(attributes.getValue(key))) + "\""));
		}
		return b.toString();
	}
	
	/**
	 * Warning: escapes only the double quotation marks! (is that officially
	 * enough, if it is to be surrounded by double quotation marks?)
	 *
	 * @param attrValue
	 */
	public static function xmlAttrEscape(attrValue:String):String {
		return attrValue.replace("\"", "&quot;");
	}
	
	public static function xmlTextEscape(text:String):String {
		return text
		.replace("&", "&amp;")
			.replace("<", "&lt;")
			.replace(">", "&gt;");
	}
	
	public static function annotationEscape(value:String):String {
		return value
		.replace("\\", "\\\\")
			.replace("\"", "\\\"")
			.replace("?", "\\q");
	}
	
	public function normalize(o:Object) {  
		// figure out what the type of o actually is and call our  
		// private helper function based on that  
		if (typeof (o) == "DocOp") {  
			normalize_docOP(DocumentOperation(o));  
		} else if (typeof (o) == "DocInitialization") {  
			normalize_DocInit(DocInitialization(o));  
		} else {  
			throw new Error("DocOpUtil.buffer  invalid type " + typeof(o));  
		}  
	} 
	
	public static function normalize_docOP(in_:DocumentOperation):BufferedDocOp {
		var n:EvaluatingDocOpCursor= new AnnotationsNormalizer(
			new RangeNormalizer(new DocOpBuffer()));
		in_.apply(n);
		return BufferedDocOp(n.finish());
	}
	
	public static function normalize_DocInit(in_:DocInitialization):BufferedDocInitialization {
		var n:EvaluatingDocOpCursor= new AnnotationsNormalizer(
			new RangeNormalizer(new DocOpBuffer()));
		in_.apply(n);
		return asInitialization(BufferedDocOp(n.finish())) as BufferedDocInitialization;
	}
	
	/**
	 * Computes the number of items of the document that an op applies to, prior
	 * to its application.
	 */
	public static function initialDocumentLength(op:DocumentOperation):int {
		var size:Array= [0];
		op.apply(new InitialDocOpCursor(size));
		return size[0];
	}
	
	/**
	 * Computes the number of items of the document that an op produces when
	 * applied.
	 */
	public static function resultingDocumentLength(op:DocumentOperation):int {
		var size:Array= [ 0];
		op.apply(new ResultingDocOpCursor(size));
		return size[0];
	}
	
	public static function visualiseOpWithDocument(doc:DocInitialization, op:DocumentOperation):Array  {
		var docB:StringBuilder= new StringBuilder();
		var opB:StringBuilder= new StringBuilder();
		var indicesB:StringBuilder= new StringBuilder();
		
		var builders:Array= [ docB, opB, indicesB ];
		var exploded:BufferedDocInitialization= ExplodedDocOp.explode(doc);
		var numDocComponents:int= exploded.size();
		var opStringifier:DocumentOperationCursor= createConciseStringBuilder(op, opB);
		
		buildXmlString(new XMLDocInitializtion(indicesB, exploded, numDocComponents, builders, opStringifier, op), -1, docB);
		
		return [docB.toString(), opB.toString(), indicesB.toString()];
	}
}
}